use craftdig_core::registrar::GameRegistrar;
use craftdig_datamodel::block::model::{BlockModel, Solidity, TexTable};
use craftdig_datamodel::block::{self, model};
use craftdig_datamodel::world::{map::ChunkState, WorldView};
use craftdig_math::*;

use godot::classes::ArrayMesh;
use godot::prelude::*;

use crate::mesh_builder::{self, MeshBuilder};

/// Contains references to a chunk and each of its neighbors so that we can make
/// some queries about the blocks' neighbors.
pub struct ChunkNeighborhood<'w> {
    coord: ChunkCoord,
    chunk: Option<&'w ChunkState>,
    neighbor_coords: [ChunkCoord; 6],
    neighbors: [Option<&'w ChunkState>; 6],
}

impl<'w> ChunkNeighborhood<'w> {
    /// Extracts the neighborhood from the world.
    pub fn extract_from_world<V: WorldView>(world: &'w V, coord: ChunkCoord) -> Self {
        let mut neighbors = [None; 6];
        let mut neighbor_coords = [coord; 6];

        // Get the center chunk.
        let chunk = world.get_chunk_ref(coord);

        // Loop through all the directions and get the chunks there too, or fill
        // in with `None` if not present.
        for (i, dir) in Direction::array().iter().enumerate() {
            let ncc = ChunkCoord::from(*coord.inner() + dir.to_unit_vec());
            neighbors[i] = world.get_chunk_ref(ncc);
            neighbor_coords[i] = ncc;
        }

        Self {
            coord,
            chunk,
            neighbor_coords,
            neighbors,
        }
    }

    /// Returns the coord of the chunk that the neighborhood is centered on.
    pub fn center_chunk_coord(&self) -> ChunkCoord {
        self.coord
    }

    /// Returns a ref to the chunk that the neighborhood is centered on, if
    /// present.
    pub fn center_chunk_state(&self) -> Option<&'w ChunkState> {
        self.chunk
    }

    /// Find the chunk for a particular coord, returning a ref to it if present.
    fn find_chunk_for_coord(&self, cc: ChunkCoord) -> Option<&'w ChunkState> {
        // TODO make this math much smarter, direct scan is fine because this
        // will all be cached and probably unrolled anyways, and most lookup
        // will be on the center chunk in the first place, but we want to be
        // able to make it just some simple math
        if cc == self.coord {
            self.chunk
        } else {
            self.neighbor_coords
                .iter()
                .enumerate()
                .find(|(_, tcc)| **tcc == cc)
                .and_then(|(i, _)| self.neighbors[i])
        }
    }

    /// Gets the block at a coord within the neighborhood.
    pub fn get_block_id(&self, coord: BlkCoord) -> Option<block::BlkId> {
        let (cc, scp) = conv_world_pos_to_chunk(coord);
        let chunk = self.find_chunk_for_coord(cc)?;
        Some(chunk.get_block_id(scp))
    }

    /// Gets the neighbor of a block at a coord within the neighborhood.
    pub fn get_block_neighbor_id(&self, coord: BlkCoord, dir: Direction) -> Option<block::BlkId> {
        let npos = BlkCoord::from(*coord.inner() + dir.to_unit_vec());
        self.get_block_id(npos)
    }
}

/// Generates a mesh for the chunk, respecting block types and such.
///
/// This mesher is focused on simplicity.  There's a lot of unnecssary detours
/// and checks that we perform when generating the mesh, so it could be
/// reasonably faster.  But at this stage in development we care about ease of
/// maintenance and iteration rather than pure performance.
pub fn gen_chunk_mesh<V: WorldView>(
    coord: ChunkCoord,
    world: &V,
    reg: &GameRegistrar,
) -> Gd<ArrayMesh> {
    let neigh = ChunkNeighborhood::extract_from_world(world, coord);
    let center_chunk = neigh.center_chunk_state().unwrap();
    let mut builder = mesh_builder::MeshBuilder::new();

    let fallback_model = BlockModel::new_simple(TexTable::new_simple(0xff));
    let fallback_textbl = TexTable::new_simple(0xff);

    // Just loop through every block, see if its neighbor is air, and generate a
    // quad on that block face if we have to.
    for scpi in 0..CHUNK_NUM_BLOCKS {
        let scp = SubchunkPos::from_raw(scpi as u16);
        let bpos = conv_chunk_poses_to_world(coord, scp);
        let fscp = Vector3::new(scp.x() as f32, scp.y() as f32, scp.z() as f32);

        let blkid = center_chunk.get_block_id(scp);

        // Don't generate anything for air.
        if blkid.is_zero() {
            continue;
        }

        let blk_data = reg.get_block_tydata_by_id(blkid);
        let model = blk_data.map(|d| d.model()).unwrap_or(&fallback_model);

        // If empty then skip.
        if model.solidity() == Solidity::Empty {
            continue;
        }

        match model.shape() {
            model::Shape::Cubic { aabb, texes } => {
                for (_i, dir) in Direction::array().into_iter().enumerate() {
                    // Query the block and figure out if it's something we should be able to see through.
                    // TODO make this be any transparent block
                    let n_data = neigh
                        .get_block_neighbor_id(bpos, dir)
                        .and_then(|id| reg.get_block_tydata_by_id(id));
                    let n_sol = n_data.map(|bd| bd.solidity()).unwrap_or(Solidity::Empty);

                    // If air then we should generate a surface.
                    if n_sol != Solidity::Opaque {
                        let face_uv_off = get_tex_pos(texes.get_dir(dir));
                        generate_block_face(&mut builder, fscp, face_uv_off, dir, aabb);
                    }
                }
            }
        }
    }

    // Finalize the mesh.
    builder.build_godot()
}

fn get_tex_pos(idx: u8) -> Vector2 {
    Vector2::new((idx & 0xf) as f32 / 16.0, (idx >> 4) as f32 / 16.0)
}

/// Generates a face for a block given offsets, using the hardcoded lookup
/// tables.  Is provided with the chunk position of the block, the texture
/// coords, and the face direction to emit, and the AABB of the block
/// structure.
///
/// UVs for AABBs are kinda broken, only works for the vertical component of
/// short blocks.
fn generate_block_face(
    builder: &mut MeshBuilder,
    wpos: Vector3,
    tpos: Vector2,
    face_dir: Direction,
    aabb: &AABB,
) {
    const TEX_W: f32 = 1.0 / 16.0;

    let [ap, bp, cp, dp] = FACE_STRUCTURE[face_dir.to_index()];
    let [(au, av), (bu, bv), (cu, cv), (du, dv)] = FACE_UV_OFFS;

    let aoff = aabb.lerp_3d(Vec3f::new(ap.0, ap.1, ap.2));
    let boff = aabb.lerp_3d(Vec3f::new(bp.0, bp.1, bp.2));
    let coff = aabb.lerp_3d(Vec3f::new(cp.0, cp.1, cp.2));
    let doff = aabb.lerp_3d(Vec3f::new(dp.0, dp.1, dp.2));

    // Hack to get half slab UVs showing up correctly, this doesn't generalize.
    let v_top_off = match face_dir {
        Direction::Up | Direction::Down => 0.0,
        _ => 1.0 - aabb.height(),
    };

    // Annoying vector conversion.
    let ap = wpos + Vector3::new(aoff.x(), aoff.y(), aoff.z());
    let bp = wpos + Vector3::new(boff.x(), boff.y(), boff.z());
    let cp = wpos + Vector3::new(coff.x(), coff.y(), coff.z());
    let dp = wpos + Vector3::new(doff.x(), doff.y(), doff.z());
    let auv = tpos + TEX_W * Vector2::new(au, av + v_top_off);
    let buv = tpos + TEX_W * Vector2::new(bu, bv);
    let cuv = tpos + TEX_W * Vector2::new(cu, cv);
    let duv = tpos + TEX_W * Vector2::new(du, dv + v_top_off);

    let norm_int = face_dir.to_unit_vec();
    let norm = Vector3::new(
        norm_int.x() as f32,
        norm_int.y() as f32,
        norm_int.z() as f32,
    );

    builder.add_triangle_norm(cp, cuv, bp, buv, ap, auv, norm);
    builder.add_triangle_norm(dp, duv, cp, cuv, ap, auv, norm);
}

/// UV offsets for the two triangles on a block face.  Diagonal goes from upper
/// left to lower right, relative to whatever "up" is.
const FACE_UV_OFFS: [(f32, f32); 4] = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)];

/// Offsets for each vertex to generate for each block face.  Corresponds with
/// indexes in [``Direction::array``], just hardcoded.
///
/// Laid out CCW in the following structure.
///
/// ```
/// 0 --- 3
/// |     |
/// |     |
/// 1 --- 2
/// ```
const FACE_STRUCTURE: [[(f32, f32, f32); 4]; 6] = [
    // East
    [
        (1.0, 1.0, 1.0),
        (1.0, 0.0, 1.0),
        (1.0, 0.0, 0.0),
        (1.0, 1.0, 0.0),
    ],
    // West
    [
        (0.0, 1.0, 0.0),
        (0.0, 0.0, 0.0),
        (0.0, 0.0, 1.0),
        (0.0, 1.0, 1.0),
    ],
    // Up
    [
        (1.0, 1.0, 0.0),
        (0.0, 1.0, 0.0),
        (0.0, 1.0, 1.0),
        (1.0, 1.0, 1.0),
    ],
    // Down
    [
        (0.0, 0.0, 0.0),
        (1.0, 0.0, 0.0),
        (1.0, 0.0, 1.0),
        (0.0, 0.0, 1.0),
    ],
    // South
    [
        (0.0, 1.0, 1.0),
        (0.0, 0.0, 1.0),
        (1.0, 0.0, 1.0),
        (1.0, 1.0, 1.0),
    ],
    // North
    [
        (1.0, 1.0, 0.0),
        (1.0, 0.0, 0.0),
        (0.0, 0.0, 0.0),
        (0.0, 1.0, 0.0),
    ],
];
